#include <array>
#include <chrono>
#include <cstring>
#include <iostream>
#include <map>

#include "ani_helpers_build_scripts.h"
#include <ani.h>

class MeasureTime final {
  using timePointT = std::chrono::time_point<std::chrono::steady_clock>;

public:
  void startTimePoint(std::string &&label);
  long long endTimePoint(timePointT endTimePoint, const std::string &label);
  void printTime(const std::string &label);
  void printTimeAll();

  static MeasureTime &getInstance() {
    static MeasureTime mt;
    return mt;
  }

private:
  MeasureTime() = default;
  void printTimeMsg(std::string_view label, long long duration) {
    std::cout << "TimePoint " << label << ": " << duration << " tics" << std::endl;
  }

  struct timeView {
    timePointT start{};
    long long duration{};
  };

  std::map<std::string, timeView> timePoints;
};

void MeasureTime::startTimePoint(std::string &&label) {
  const auto &[it, wasInserted] = timePoints.try_emplace(std::move(label), timePointT(), 0);
  if (!wasInserted) {
    INTEROP_FATAL("impl_startTimePoint: label already exist: %s", label.data());
  }

  it->second.start = std::chrono::steady_clock::now();
}

long long MeasureTime::endTimePoint(timePointT endTimePoint, const std::string &label) {
  auto it = timePoints.find(label);
  if (it == timePoints.end()) {
    INTEROP_FATAL("impl_endTimePoint: label is missing: %s", label.data());
  }

  long long duration = (endTimePoint - it->second.start).count();

  it->second.duration = duration;
  return duration;
}

void MeasureTime::printTime(const std::string &label) {
  auto it = timePoints.find(label);
  if (it == timePoints.end()) {
    INTEROP_FATAL("impl_printTime: label is missing: %s", label.data());
  }

  printTimeMsg(label, it->second.duration);
}

void MeasureTime::printTimeAll() {
  for (const auto &[label, timeData] : timePoints) {
    printTimeMsg(label, timeData.duration);
  }
}

static void impl_startTimePoint(ani_env *env, [[maybe_unused]] ani_class cls, ani_string labelAni) {
  MeasureTime::getInstance().startTimePoint(getAniString(env, labelAni));
}

static ani_long impl_endTimePoint(ani_env *env, [[maybe_unused]] ani_class cls, ani_string labelAni) {
  auto endTimePoint = std::chrono::steady_clock::now();
  auto endTime = MeasureTime::getInstance().endTimePoint(endTimePoint, getAniString(env, labelAni));
  return static_cast<ani_long>(endTime);
}

static void impl_startTimePointI(ani_int labelInt) {
  MeasureTime::getInstance().startTimePoint(std::to_string(labelInt));
}

static ani_long impl_endTimePointI(ani_int labelInt) {
  auto endTimePoint = std::chrono::steady_clock::now();
  auto endTime = MeasureTime::getInstance().endTimePoint(endTimePoint, std::to_string(labelInt));
  return static_cast<ani_long>(endTime);
}

static void impl_printTime(ani_env *env, [[maybe_unused]] ani_class cls, ani_string labelAni) {
  MeasureTime::getInstance().printTime(getAniString(env, labelAni));
}

static void impl_printTimeAll() { MeasureTime::getInstance().printTimeAll(); }

extern "C" __attribute__((weak)) int StartSamplingProfiler(const char *asptFilename, int interva) {
  std::cout << "Link error: StartSamplingProfiler was not loaded" << std::endl;
  abort();
}

extern "C" __attribute__((weak)) void StopSamplingProfiler() {
  std::cout << "Link error: StopSamplingProfiler was not loaded" << std::endl;
  abort();
}

static void impl_startProfile([[maybe_unused]] ani_env *env, [[maybe_unused]] ani_class cls, ani_string aniFilePath,
                              ani_int interval) {
  std::cout << "impl_startProfile " << std::endl;

  std::cout << "interval " << interval << std::endl;

  std::string filePath = getAniString(env, aniFilePath);

  std::cout << "filePath: " << filePath << std::endl;

  int res = StartSamplingProfiler(filePath.c_str(), interval);
}

static void impl_stopProfile() {
  std::cout << "impl_stopProfile " << std::endl;
  StopSamplingProfiler();
}

ANI_EXPORT ani_status ANI_Constructor(ani_vm *vm, uint32_t *result) {
  ani_env *env;
  CHECK_ANI_FATAL(vm->GetEnv(ANI_VERSION_1, &env));

  static const char *className = "MeasureNativeLib.MeasureNativeLib";
  ani_class cls;
  CHECK_ANI_FATAL(env->FindClass(className, &cls));

  std::array staticMethods = {
      ani_native_function{"startTimePoint", "C{std.core.String}:", reinterpret_cast<void *>(impl_startTimePoint)},
      ani_native_function{"endTimePoint", "C{std.core.String}:l", reinterpret_cast<void *>(impl_endTimePoint)},
      ani_native_function{"startTimePointI", "i:", reinterpret_cast<void *>(impl_startTimePointI)},
      ani_native_function{"endTimePointI", "i:l", reinterpret_cast<void *>(impl_endTimePointI)},
      ani_native_function{"printTime", "C{std.core.String}:", reinterpret_cast<void *>(impl_printTime)},
      ani_native_function{"printTimeAll", ":", reinterpret_cast<void *>(impl_printTimeAll)},
      ani_native_function{"startProfile", "C{std.core.String}i:", reinterpret_cast<void *>(impl_startProfile)},
      ani_native_function{"stopProfile", ":", reinterpret_cast<void *>(impl_stopProfile)},
  };

  CHECK_ANI_FATAL(env->Class_BindStaticNativeMethods(cls, staticMethods.data(), staticMethods.size()));

  *result = ANI_VERSION_1;
  return ANI_OK;
}
